<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
* A base model with a series of CRUD functions (powered by CI's query builder),
* validation-in-model support, event callbacks and more.
*
* @link http://github.com/jamierumbelow/codeigniter-base-model
* @copyright Copyright (c) 2012, Jamie Rumbelow <http://jamierumbelow.net>
*/

class Base_model extends CI_Model {
	
	/*
	 * -------------------------------------------------------------- VARIABLES
	 * ------------------------------------------------------------
	 */
	
	/**
	 * This model's default database table.
	 * Automatically
	 * guessed by pluralising the model name.
	 */
	public $tablename;
	
	/**
	 * The database connection object.
	 * Will be set to the default
	 * connection. This allows individual models to use different DBs
	 * without overwriting CI's global $this->db connection.
	 */
	public $_database;
	
	/**
	 * This model's default primary key or unique identifier.
	 * Used by the get(), update() and delete() functions.
	 */
	protected $primary_key = 'id';
	
	/**
	 * Support for soft deletes and this model's 'deleted' key
	 */
	protected $soft_delete = FALSE;
	protected $soft_delete_key = 'deleted';
	protected $_temporary_with_deleted = FALSE;
	protected $_temporary_only_deleted = FALSE;
	
	/**
	 * The various callbacks available to the model.
	 * Each are
	 * simple lists of method names (methods will be run on $this).
	 */
	protected $before_create = array ();
	protected $after_create = array ();
	protected $before_update = array ();
	protected $after_update = array ();
	protected $before_get = array ();
	protected $after_get = array ();
	protected $before_delete = array ();
	protected $after_delete = array ();
	
	protected $callback_parameters = array ();
	
	/**
	 * Protected, non-modifiable attributes
	 */
	protected $protected_attributes = array ();
	
	/**
	 * Relationship arrays.
	 * Use flat strings for defaults or string
	 * => array to customise the class name and primary key
	 */
	protected $belongs_to = array ();
	protected $has_many = array ();
	
	protected $_with = array ();
	
	/**
	 * An array of validation rules.
	 * This needs to be the same format
	 * as validation rules passed to the Form_validation library.
	 */
	protected $validate = array ();
	
	/**
	 * Optionally skip the validation.
	 * Used in conjunction with
	 * skip_validation() to skip data validation for any future calls.
	 */
	protected $skip_validation = FALSE;
	
	/**
	 * By default we return our results as objects.
	 * If we need to override
	 * this, we can, or, we could use the `as_array()` and `as_object()` scopes.
	 */
	protected $return_type = 'object';
	protected $_temporary_return_type = NULL;
	
	/*
	 * -------------------------------------------------------------- GENERIC
	 * METHODS ------------------------------------------------------------
	 */
	
	/**
	 * Initialise the model, tie into the CodeIgniter superobject and
	 * try our best to guess the table name.
	 */
	public function __construct() {
		parent::__construct ();
		$this->load->helper ( 'inflector' );
		$this->_fetch_table ();
		$this->_database = $this->db;
		array_unshift ( $this->before_create, 'protect_attributes' );
		array_unshift ( $this->before_update, 'protect_attributes' );
		$this->_temporary_return_type = $this->return_type;
		$this->set_tablename();
	}
	
	private function set_tablename(){
		if ( empty($this->tablename) ){
			$this->tablename = str_replace('_model', '', strtolower(get_class($this)));
		}
	}
	
	/*
	 * -------------------------------------------------------------- CRUD
	 * INTERFACE ------------------------------------------------------------
	 */
	
	/**
	 * Fetch a single record based on the primary key.
	 * Returns an object.
	 */
	public function get($primary_value) {
		return $this->get_by ( $this->primary_key, $primary_value );
	}
	
	/**
	 * Fetch a single record based on an arbitrary WHERE call.
	 * Can be
	 * any valid value to $this->_database->where().
	 */
	public function get_by() {
		$where = func_get_args ();
		
		if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) {
			$this->_database->where ( $this->soft_delete_key, ( bool ) $this->_temporary_only_deleted );
		}
		
		$this->_set_where ( $where );
		$this->trigger ( 'before_get' );
		$row = $this->_database->get ( $this->tablename )->{$this->_return_type ()} ();
		$this->_temporary_return_type = $this->return_type;
		
		$row = $this->trigger ( 'after_get', $row );
		
		$this->_with = array ();
		return $row;
	}
	
	/**
	 * Fetch an array of records based on an arbitrary WHERE call.
	 */
	public function get_many_by() {
		$where = func_get_args ();
		if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) {
			$this->_database->where ( $this->soft_delete_key, ( bool ) $this->_temporary_only_deleted );
		}
		$this->_set_where ( $where );
		return $this->get_all ();
	}
	
	/**
	 * Fetch all the records in the table.
	 * Can be used as a generic call
	 * to $this->_database->get() with scoped methods.
	 */
	public function get_all() {
		$this->trigger ( 'before_get' );
		
		if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) {
			$this->_database->where ( $this->soft_delete_key, ( bool ) $this->_temporary_only_deleted );
		}
		$result = $this->_database->get ( $this->tablename )->{$this->_return_type ( 1 )} ();
		$this->_temporary_return_type = $this->return_type;
		
		foreach ( $result as $key => &$row ) {
			$row = $this->trigger ( 'after_get', $row, ($key == count ( $result ) - 1) );
		}
		
		$this->_with = array ();
		return $result;
	}
	
	/**
	 * Insert a new row into the table.
	 * $data should be an associative array
	 * of data to be inserted. Returns newly created ID.
	 */
	public function insert($data, $skip_validation = FALSE) {
		if ($skip_validation === FALSE) {
			$data = $this->validate ( $data );
		}
		if ($data !== FALSE) {
			$data = $this->trigger ( 'before_create', $data );
			$this->_database->insert ( $this->tablename, $data );
			$insert_id = $this->_database->insert_id ();
			$this->trigger ( 'after_create', $insert_id );
			return $insert_id;
		} else {
			return FALSE;
		}
	}
	
	/**
	 * Insert multiple rows into the table.
	 * Returns an array of multiple IDs.
	 */
	public function batch_insert($data, $skip_validation = FALSE) {
		$ids = array ();
		foreach ( $data as $key => $row ) {
			$ids [] = $this->insert ( $row, $skip_validation, ($key == count ( $data ) - 1) );
		}
		return $ids;
	}
	
	/**
	 * Updated a record based on the primary value.
	 */
	public function update($primary_value, $data, $skip_validation = FALSE) {
		$data = $this->trigger ( 'before_update', $data );
		
		if ($skip_validation === FALSE) {
			$data = $this->validate ( $data );
		}
		
		if ($data !== FALSE) {
			$result = $this->_database->where ( $this->primary_key, $primary_value )->set ( $data )->update ( $this->tablename );
			$this->trigger ( 'after_update', array (
					$data,
					$result 
			) );
			return $result;
		} else {
			return FALSE;
		}
	}
	
	/**
	 * Update many records, based on an array of primary values.
	 */
	public function batch_update($primary_values, $data, $skip_validation = FALSE) {
		$data = $this->trigger ( 'before_update', $data );
		if ($skip_validation === FALSE) {
			$data = $this->validate ( $data );
		}
		if ($data !== FALSE) {
			$result = $this->_database->where_in ( $this->primary_key, $primary_values )->set ( $data )->update ( $this->tablename );
			$this->trigger ( 'after_update', array (
					$data,
					$result 
			) );
			return $result;
		} else {
			return FALSE;
		}
	}
	
	/**
	 * Updated a record based on an arbitrary WHERE clause.
	 */
	public function update_by() {
		$args = func_get_args ();
		$data = array_pop ( $args );
		$data = $this->trigger ( 'before_update', $data );
		if ($this->validate ( $data ) !== FALSE) {
			$this->_set_where ( $args );
			$result = $this->_database->set ( $data )->update ( $this->tablename );
			$this->trigger ( 'after_update', array (
					$data,
					$result 
			) );
			return $result;
		} else {
			return FALSE;
		}
	}
	
	/**
	 * Update all records
	 */
	public function update_all($data) {
		$data = $this->trigger ( 'before_update', $data );
		$result = $this->_database->set ( $data )->update ( $this->tablename );
		$this->trigger ( 'after_update', array (
				$data,
				$result 
		) );
		return $result;
	}
	
	/**
	 * Delete a row from the table by the primary value
	 */
	public function delete($id) {
		$this->trigger ( 'before_delete', $id );
		$this->_database->where ( $this->primary_key, $id );
		if ($this->soft_delete) {
			$result = $this->_database->update ( $this->tablename, array (
					$this->soft_delete_key => TRUE 
			) );
		} else {
			$result = $this->_database->delete ( $this->tablename );
		}
		$this->trigger ( 'after_delete', $result );
		return $result;
	}
	
	/**
	 * Delete a row from the database table by an arbitrary WHERE clause
	 */
	public function delete_by() {
		$where = func_get_args ();
		$where = $this->trigger ( 'before_delete', $where );
		$this->_set_where ( $where );
		if ($this->soft_delete) {
			$result = $this->_database->update ( $this->tablename, array (
					$this->soft_delete_key => TRUE 
			) );
		} else {
			$result = $this->_database->delete ( $this->tablename );
		}
		$this->trigger ( 'after_delete', $result );
		return $result;
	}
	
	/**
	 * Delete many rows from the database table by multiple primary values
	 */
	public function batch_delete($primary_values) {
		$primary_values = $this->trigger ( 'before_delete', $primary_values );
		$this->_database->where_in ( $this->primary_key, $primary_values );
		if ($this->soft_delete) {
			$result = $this->_database->update ( $this->tablename, array (
					$this->soft_delete_key => TRUE 
			) );
		} else {
			$result = $this->_database->delete ( $this->tablename );
		}
		$this->trigger ( 'after_delete', $result );
		return $result;
	}
	
	/**
	 * Truncates the table
	 */
	public function truncate() {
		$result = $this->_database->truncate ( $this->tablename );
		return $result;
	}
	
	/*
	 * --------------------------------------------------------------
	 * RELATIONSHIPS
	 * ------------------------------------------------------------
	 */
	
	public function with($relationship) {
		$this->_with [] = $relationship;
		if (! in_array ( 'relate', $this->after_get )) {
			$this->after_get [] = 'relate';
		}
		return $this;
	}
	
	public function relate($row) {
		if (empty ( $row )) {
			return $row;
		}
		
		foreach ( $this->belongs_to as $key => $value ) {
			if (is_string ( $value )) {
				$relationship = $value;
				$options = array (
						'primary_key' => $value . '_id',
						'model' => $value . '_model' 
				);
			} else {
				$relationship = $key;
				$options = $value;
			}
			
			if (in_array ( $relationship, $this->_with )) {
				$this->load->model ( $options ['model'], $relationship . '_model' );
				
				if (is_object ( $row )) {
					$row->{$relationship} = $this->{$relationship . '_model'}->get ( $row->{$options ['primary_key']} );
				} else {
					$row [$relationship] = $this->{$relationship . '_model'}->get ( $row [$options ['primary_key']] );
				}
			}
		}
		
		foreach ( $this->has_many as $key => $value ) {
			if (is_string ( $value )) {
				$relationship = $value;
				$options = array (
						'primary_key' => singular ( $this->tablename ) . '_id',
						'model' => singular ( $value ) . '_model' 
				);
			} else {
				$relationship = $key;
				$options = $value;
			}
			
			if (in_array ( $relationship, $this->_with )) {
				$this->load->model ( $options ['model'], $relationship . '_model' );
				
				if (is_object ( $row )) {
					$row->{$relationship} = $this->{$relationship . '_model'}->get_many_by ( $options ['primary_key'], $row->{$this->primary_key} );
				} else {
					$row [$relationship] = $this->{$relationship . '_model'}->get_many_by ( $options ['primary_key'], $row [$this->primary_key] );
				}
			}
		}
		
		return $row;
	}
	
	/*
	 * -------------------------------------------------------------- UTILITY
	 * METHODS ------------------------------------------------------------
	 */
	
	/**
	 * Retrieve and generate a form_dropdown friendly array
	 */
	function dropdown() {
		$args = func_get_args ();
		
		if (count ( $args ) == 2) {
			list ( $key, $value ) = $args;
		} else {
			$key = $this->primary_key;
			$value = $args [0];
		}
		
		$this->trigger ( 'before_dropdown', array (
				$key,
				$value 
		) );
		
		if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) {
			$this->_database->where ( $this->soft_delete_key, FALSE );
		}
		
		$result = $this->_database->select ( array (
				$key,
				$value 
		) )->get ( $this->tablename )->result ();
		$options = array ();
		foreach ( $result as $row ) {
			$options [$row->{$key}] = $row->{$value};
		}
		$options = $this->trigger ( 'after_dropdown', $options );
		return $options;
	}
	
	/**
	 * Fetch a count of rows based on an arbitrary WHERE call.
	 */
	public function count_by() {
		$where = func_get_args ();
		$this->_set_where ( $where );
		return $this->_database->count_all_results ( $this->tablename );
	}
	
	/**
	 * Fetch a total count of rows, disregarding any previous conditions
	 */
	public function count_all() {
		return $this->_database->count_all ( $this->tablename );
	}
	
	/**
	 * Tell the class to skip the insert validation
	 */
	public function skip_validation() {
		$this->skip_validation = TRUE;
		return $this;
	}
	
	/**
	 * Get the skip validation status
	 */
	public function get_skip_validation() {
		return $this->skip_validation;
	}
	
	/**
	 * Return the next auto increment of the table.
	 * Only tested on MySQL.
	 */
	public function get_next_id() {
		return ( int ) $this->_database->select ( 'AUTO_INCREMENT' )->from ( 'information_schema.TABLES' )->where ( 'TABLE_NAME', $this->tablename )->where ( 'TABLE_SCHEMA', $this->_database->database )->get ()->row ()->AUTO_INCREMENT;
	}
	
	/*
	 * -------------------------------------------------------------- GLOBAL
	 * SCOPES ------------------------------------------------------------
	 */
	
	/**
	 * Return the next call as an array rather than an object
	 */
	public function as_array() {
		$this->_temporary_return_type = 'array';
		return $this;
	}
	
	/**
	 * Return the next call as an object rather than an array
	 */
	public function as_object() {
		$this->_temporary_return_type = 'object';
		return $this;
	}
	
	/**
	 * Don't care about soft deleted rows on the next call
	 */
	public function with_deleted() {
		$this->_temporary_with_deleted = TRUE;
		return $this;
	}
	
	/**
	 * Only get deleted rows on the next call
	 */
	public function only_deleted() {
		$this->_temporary_only_deleted = TRUE;
		return $this;
	}
	
	/*
	 * -------------------------------------------------------------- OBSERVERS
	 * ------------------------------------------------------------
	 */
	
	/**
	 * MySQL DATETIME created_at and updated_at
	 */
	public function created_at($row) {
		if (is_object ( $row )) {
			$row->created_at = date ( 'Y-m-d H:i:s' );
		} else {
			$row ['created_at'] = date ( 'Y-m-d H:i:s' );
		}
		
		return $row;
	}
	
	public function updated_at($row) {
		if (is_object ( $row )) {
			$row->updated_at = date ( 'Y-m-d H:i:s' );
		} else {
			$row ['updated_at'] = date ( 'Y-m-d H:i:s' );
		}
		
		return $row;
	}
	
	/**
	 * Serialises data for you automatically, allowing you to pass
	 * through objects and let it handle the serialisation in the background
	 */
	public function serialize($row) {
		foreach ( $this->callback_parameters as $column ) {
			$row [$column] = serialize ( $row [$column] );
		}
		
		return $row;
	}
	
	public function unserialize($row) {
		foreach ( $this->callback_parameters as $column ) {
			if (is_array ( $row )) {
				$row [$column] = unserialize ( $row [$column] );
			} else {
				$row->$column = unserialize ( $row->$column );
			}
		}
		
		return $row;
	}
	
	/**
	 * Protect attributes by removing them from $row array
	 */
	public function protect_attributes($row) {
		foreach ( $this->protected_attributes as $attr ) {
			if (is_object ( $row )) {
				unset ( $row->$attr );
			} else {
				unset ( $row [$attr] );
			}
		}
		
		return $row;
	}
	
	/*
	 * -------------------------------------------------------------- QUERY
	 * BUILDER DIRECT ACCESS METHODS
	 * ------------------------------------------------------------
	 */
	
	/**
	 * A wrapper to $this->_database->order_by()
	 */
	public function order_by($criteria, $order = 'ASC') {
		if (is_array ( $criteria )) {
			foreach ( $criteria as $key => $value ) {
				$this->_database->order_by ( $key, $value );
			}
		} else {
			$this->_database->order_by ( $criteria, $order );
		}
		return $this;
	}
	
	/**
	 * A wrapper to $this->_database->limit()
	 */
	public function limit($limit, $offset = 0) {
		$this->_database->limit ( $limit, $offset );
		return $this;
	}
	
	/*
	 * -------------------------------------------------------------- INTERNAL
	 * METHODS ------------------------------------------------------------
	 */
	
	/**
	 * Trigger an event and call its observers.
	 * Pass through the event name
	 * (which looks for an instance variable $this->event_name), an array of
	 * parameters to pass through and an optional 'last in interation' boolean
	 */
	public function trigger($event, $data = FALSE, $last = TRUE) {
		if (isset ( $this->$event ) && is_array ( $this->$event )) {
			foreach ( $this->$event as $method ) {
				if (strpos ( $method, '(' )) {
					preg_match ( '/([a-zA-Z0-9\_\-]+)(\(([a-zA-Z0-9\_\-\., ]+)\))?/', $method, $matches );
					
					$method = $matches [1];
					$this->callback_parameters = explode ( ',', $matches [3] );
				}
				$data = call_user_func_array ( array (
						$this,
						$method 
				), array (
						$data,
						$last 
				) );
			}
		}
		
		return $data;
	}
	
	/**
	 * Run validation on the passed data
	 */
	public function validate($data) {
		if ($this->skip_validation) {
			return $data;
		}
		
		if (! empty ( $this->validate )) {
			foreach ( $data as $key => $val ) {
				$_POST [$key] = $val;
			}
			$this->load->library ( 'form_validation' );
			if (is_array ( $this->validate )) {
				$this->form_validation->set_rules ( $this->validate );
				if ($this->form_validation->run () === TRUE) {
					return $data;
				} else {
					return FALSE;
				}
			} else {
				if ($this->form_validation->run ( $this->validate ) === TRUE) {
					return $data;
				} else {
					return FALSE;
				}
			}
		} else {
			return $data;
		}
	}
	
	/**
	 * Guess the table name by pluralising the model name
	 */
	private function _fetch_table() {
		if ($this->tablename == NULL) {
			$this->tablename = plural ( preg_replace ( '/(_m|_model)?$/', '', strtolower ( get_class ( $this ) ) ) );
		}
	}
	
	/**
	 * Set WHERE parameters, cleverly
	 */
	protected function _set_where($params) {
		if (count ( $params ) == 1) {
			$this->_database->where ( $params [0] );
		} else if (count ( $params ) == 2) {
			$this->_database->where ( $params [0], $params [1] );
		} else if (count ( $params ) == 3) {
			$this->_database->where ( $params [0], $params [1], $params [2] );
		} else {
			$this->_database->where ( $params );
		}
	}
	
	/**
	 * Return the method name for the current return type
	 */
	protected function _return_type($multi = FALSE) {
		$method = ($multi) ? 'result' : 'row';
		return $this->_temporary_return_type == 'array' ? $method . '_array' : $method;
	}
	
	
	public function select($params=array(),$limit=''){
		if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) {
			$this->_database->where ( $this->soft_delete_key, ( bool ) $this->_temporary_only_deleted );
		}
		$this->trigger ( 'before_get' );
		if (count($params)){
			foreach ($params as $key=>$param){
				if ($key=='where'){
					foreach ($param as $k=>$fieldvalue){
						$this->_database->where($k,$fieldvalue);
					}
				}
				elseif ($key=='or_where'){
					foreach ($param as $k=>$fieldvalue){
						$this->_database->or_where($k,$fieldvalue);
					}
				}
				elseif ($key=='join'){
					foreach ($params as $k=>$fieldvalue){
						$this->_database->join($fieldvalue[0],$fieldvalue[1],$fieldvalue[2]);
					}
				}
			}
		}
		else{
			$this->_database->where ( $params );
		}
		$row = $this->_database->get ( $this->tablename )->{$this->_return_type (true)} ();
		$this->trigger ( 'after_get',$row );
		return $row;
	}
}